#include <Arduino.h>
#include "DeviceProtocol.hpp"
#include "TimerOne.h"

using WaveCaculator = uint8_t (*)(unsigned long t, unsigned long p);

uint8_t SinWave(unsigned long t, unsigned long p) {
    if(p == 0) {
        return 0;
    }
    double x = t % p;
    x = radians((x * 360.0 / p) - 90.0);
    return static_cast<uint8_t>(round(127.5 * (sin(x) + 1.0)));
}

uint8_t SawtoothWave(unsigned long t, unsigned long p) {
    if(p == 0) {
        return 0;
    }
    t %= p;
    return static_cast<uint8_t>(round(double(t) * 255 / double(p)));
}

uint8_t TriangleWave(unsigned long t, unsigned long p) {
    if(p == 0) {
        return 0;
    }
    t %= p;
    if(t >= p/2) {
        t = p - t;
    }
    return static_cast<uint8_t>(round(double(t) * 510 / double(p)));
}

class LedController {
public:
    LedController() = default;

    void Init() {
        // led in
        pinMode(LED_BUILTIN, INPUT);
        // pwm out
        pinMode(11, OUTPUT);
        digitalWrite(11, LOW);
        // fast pwm
        TCCR2A = _BV(COM2A1) | _BV(WGM21) | _BV(WGM20);
        // 1/64 clk
        TCCR2B = _BV(CS22);
        // 0%
        SetLightness(0);

        ResetTimeBase();
    }

    void ResetTimeBase() {
        msTimeBase = millis();
    }

    void SetLightness(uint8_t lightness) {
        OCR2A = lightness;
    }

    void SetWaveMode(WaveCaculator cb, unsigned long msPeriod) {
        waveCaculator = cb;
        msWavePeriod = msPeriod;
    }

    void ClearWaveMode() {
        waveCaculator = nullptr;
        msWavePeriod = 0;
    }

    bool UpdateLightnessInWaveMode() {
        if(waveCaculator && msWavePeriod > 100) {
            SetLightness(waveCaculator(millis() - msTimeBase, msWavePeriod));
            return true;
        }
        return false;
    }

    void GetCurrentStatus(unsigned long &msRunTime, uint8_t &lightness) {
        lightness = OCR2A;
        msRunTime = millis() - msTimeBase;
    }

private:
    unsigned long msTimeBase = 0;
    unsigned long msWavePeriod = 0;
    WaveCaculator waveCaculator = nullptr;
};

static uint8_t frameInputBuffer[256]{};
static uint8_t frameOutputBuffer[256]{};

static void TimerISR();

class DeviceModel {
public:
    DeviceModel() 
        : protoDecoder(frameInputBuffer, proto::FrameAddrDevice)
        , protoEncoder(frameOutputBuffer, proto::FrameAddrHost)
    {}
    
    void Initialize() {
        ledController.Init();
        Timer1.initialize(4000);
        Timer1.attachInterrupt(TimerISR);
    }

    void OnRecvDeviceData(uint8_t *dataBytes, int byteCnt) {
        if (!dataBytes || !byteCnt) {
            return;
        }
        for (int i = 0; i < byteCnt; i++) {
            auto status = protoDecoder.AppendByte(dataBytes[i]);
            if (status == proto::FrameDecoder::Status::DecodeFailed || 
                status == proto::FrameDecoder::Status::BadCrc
            ) {
                protoDecoder.Reset();
                i--;
                continue;
            }
            else if (status == proto::FrameDecoder::Status::FrameEnd) {
                protoDecoder.GetTLVs(DeviceModel::OnNewTlv, this);
            }
        }
    }

    void SendAck2Host(uint16_t targetCrc) {
        // [tag][2][data_crc0][data_crc1]
        uint8_t value[2]{};
        value[0] = static_cast<uint8_t>(targetCrc & 0xff);
        targetCrc >>= 8;
        value[1] = static_cast<uint8_t>(targetCrc & 0xff);
        protoEncoder.ClearFrame();
        protoEncoder.AppendTLV(proto::Tag2Num(proto::FrameTag::DataAck), 2, value);
        protoEncoder.AppendCRC();
        size_t frameLen = 0;
        auto frame = protoEncoder.GetFrame(frameLen);
        if(!frame) {
            return;
        }
        Serial.write(frame, frameLen);
    }

    bool TrigReport() {
        if(!reportEnable) {
            return false;
        }
        reportWaiting = true;
        return true;
    }

    bool CheckReportEvent() {
        if(reportEnable && reportWaiting) {
            reportWaiting = false;
            ReportLedStatus();
            return true;
        }
        return false;
    }

    bool CheckPwmEvent() {
        if(pwmUpdateWaiting) {
            pwmUpdateWaiting = false;
            return ledController.UpdateLightnessInWaveMode();
        }
        return false;
    }

    // run in timer ISR (4ms)
    void TimerTick() {
        // 250Hz
        pwmUpdateWaiting = true;
        // 50Hz
        timerTickCnt++;
        if(timerTickCnt > 4) {
            timerTickCnt = 0;
            TrigReport();
        }
    }

private:
    void ReportLedStatus() {
        uint8_t lightness = 0;
        unsigned long msRunTime = 0;
        ledController.GetCurrentStatus(msRunTime, lightness);
        // [tag][03][light][t0][t1]
        // [tag][04][light][t0][t1][t2]
        // [tag][05][light][t0][t1][t2][t3]
        uint8_t value[5]{};
        value[0] = lightness;
        
        uint8_t len = 0;
        if(msRunTime <= 0xffff) {
            len = 3;
        }
        else if(msRunTime < 0xffffff) {
            len = 4;
        }
        else {
            len = 5;
        }
        for(uint8_t i = 1; i < len; i++) {
            value[i] = msRunTime & 0xff;
            msRunTime >>= 8;
        }

        protoEncoder.ClearFrame();
        protoEncoder.AppendTLV(proto::Tag2Num(proto::FrameTag::ReportLedStatus), len, value);
        protoEncoder.AppendCRC();
        size_t frameLen = 0;
        auto frame = protoEncoder.GetFrame(frameLen);
        if(!frame) {
            return;
        }
        Serial.write(frame, frameLen);
    }

    static bool OnNewTlv(void* userdata, uint8_t t, uint8_t l, const uint8_t *v) {
        if(userdata) {
            return static_cast<DeviceModel*>(userdata)->OnNewTlv(t, l, v);
        }
        return false;
    }

    bool OnNewTlv(uint8_t tag, uint8_t len, const uint8_t *val) {
        switch (proto::FrameTag(tag))
		{
        case proto::FrameTag::ResetTimeBase:
            OnCmdResetLedTimeBase(val, len);
            break;
        case proto::FrameTag::LedStatusReportCfg:
            OnCmdConfigLedStatusReport(val, len);
            break;
        case proto::FrameTag::PwmFixedDutyCycle:
            OnCmdFixedDutyCycle(val, len);
            break;
        case proto::FrameTag::PwmSawtoothWave:
            OnCmdConfigWaveMode(SawtoothWave, val, len);
            break;
        case proto::FrameTag::PwmTriangleWave:
            OnCmdConfigWaveMode(TriangleWave, val, len);
            break;
        case proto::FrameTag::PwmSinWave:
            OnCmdConfigWaveMode(SinWave, val, len);
            break;
        default:
            break;
        }
        return true;
    }

    void OnCmdResetLedTimeBase(const uint8_t*, uint8_t) {
        // [tag][00]
        ledController.ResetTimeBase();
    }

    void OnCmdConfigLedStatusReport(const uint8_t* val, int len) {
        // [tag][01][start(1)]
        // [tag][01][stop(0)]
        if(!val || len < 1) {
            return;
        }
        if(val[0] == 0) {
            // stop report
            reportEnable = false;
        }
        else if(val[0] == 1) {
            // start report
            reportEnable = true;
        }
    }

    void OnCmdFixedDutyCycle(const uint8_t* val, int len) {
        // [tag][01][fdc(0~255)]
        if(val && len > 0) {
            ledController.ClearWaveMode();
            ledController.SetLightness(val[0]);
        }
    }

    void OnCmdConfigWaveMode(WaveCaculator cb, const uint8_t* val, int len) {
        // [tag][02][period_low][period_high]
        if(!val || len < 2) {
            return;
        }
        unsigned long period = val[1];
        period <<= 8;
        period += val[0];
        ledController.SetWaveMode(cb, period);
    }

private:
    proto::FrameDecoder protoDecoder;
	proto::FrameAssembler protoEncoder;
    LedController ledController;
    bool reportEnable = false;
    bool reportWaiting = false;
    bool pwmUpdateWaiting = false;
    uint8_t timerTickCnt = 0;
} deviceModel;

// 4ms
void TimerISR() {
    deviceModel.TimerTick();
}

void setup() {
    Serial.begin(115200);
    deviceModel.Initialize();
}

void loop() {
    deviceModel.CheckPwmEvent();
    deviceModel.CheckReportEvent();
    while(Serial.available() > 0) {
        static uint8_t buffer[SERIAL_RX_BUFFER_SIZE]{};
        auto bytesCnt = min(SERIAL_RX_BUFFER_SIZE, Serial.available());
        Serial.readBytes(buffer, bytesCnt);
        deviceModel.OnRecvDeviceData(buffer, bytesCnt);
    }
}
